﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Timers;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.GamerServices;
using Game_Engine;
#endregion

namespace Project
{
    public class Project : Game
    {
        //**************************************//
        //*******GUI DATA***********************//
        class Scene
        {
            public delegate void CallMethod();
            public CallMethod Update;
            public CallMethod Draw;
            public Scene(CallMethod update, CallMethod draw)
            { Update = update; Draw = draw; }
        }

        Dictionary<String, Scene> scenes;
        Scene currentScene;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont font;
        StreamReader sr;
        StringBuilder instructions, instructionsNext;
        Sprite instructionsSprite1, instructionsSprite2;

        Color backgroundColor, checkColor, textColor;

        Texture2D texture;

        List<GUIElement> menuElements, pauseElements;
        /*****************************************************/
        //***************************************************//

        //******************GAME VARIABLES*******************//
        //***************************************************//
        //Our graphical assets
        Model sphere, box;
        Texture2D heightMap, normalMap;
        Material terrainMaterial;
        Effect terrainEffect;
        Light light;

        //GUI Graphics
        StringBuilder textStream;
        float seconds;
        int minutes;
        String displayTime;
        Label timeBox;
        GUIGroup speedometer;
        GUIElement speedtick;
        UpdatingText textBox;

        //Player view
        Camera camera;
        GameObject cameraObject;
        float cameraSpeed = 0.01f;
        float rotation = 0;
        float zoom = (float)Math.PI / 2;

        //The player
        GameObject player, movementObject;
        float moveSpeed = 1.0f;
        float verticalSpeed = 0.0f;

        //The environment
        GameObject building, terrain;
        List<GameObject> buildings;

        //Gravity - Which should only be applied under certain conditions
        //Namely: Are you in the air?
        float gravity = 0.5f;

        //Input
        InputAxis horizontal, vertical, cameraUp, cameraSide, cameraZoom;

        //Important others
        Random random;
        Vector3 normal;         //For all of your collision needs
        
        //****************************************************************//
        //****************************************************************//

        public Project()
            : base()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            Time.Initialize();
            InputManager.Initialize();
            ScreenManager.Initialize(graphics);
            base.Initialize();

            /**************************************************************************************************/
            //////////////////////////////GUI INITIALIZATION////////////////////////////////////////////////////
            backgroundColor = Color.Blue;
            textColor = Color.Black;
            checkColor = Color.Yellow;

            menuElements = new List<GUIElement>();
            pauseElements = new List<GUIElement>();
            scenes = new Dictionary<string, Scene>();

            GUIGroup menuGroup = new GUIGroup();

            Button playButton = new Button(texture, new Rectangle(50, 50, 300, 20), "Play", backgroundColor, textColor);
            playButton.Action += StartGame;

            Button instructionButton = new Button(texture, new Rectangle(60, 70, 300, 20), "Instructions", backgroundColor, textColor);
            instructionButton.Action += ShowGameInstructions;

            CheckBox optionBox = new CheckBox(texture, texture, new Rectangle(55, 90, 300, 20), "Full Screen", backgroundColor, textColor, checkColor);
            optionBox.Action += MakeFullScreen;

            Button exitButton = new Button(texture, new Rectangle(65, 110, 300, 20), "Exit", backgroundColor, textColor);
            exitButton.Action += ExitGame;

            menuGroup.Children.Add(playButton);
            menuGroup.Children.Add(instructionButton);
            menuGroup.Children.Add(optionBox);
            menuGroup.Children.Add(exitButton);
            menuGroup.Children[menuGroup.SelectedIndex].Selected = true;
            menuElements.Add(menuGroup);

            GUIGroup pauseGroup = new GUIGroup();

            Button resumeButton = new Button(texture, new Rectangle(ScreenManager.Width / 3, ScreenManager.Height / 3, ScreenManager.Width / 3, 20), "Resume", backgroundColor, textColor);
            resumeButton.Action += StartGame;

            Button menuButton = new Button(texture, new Rectangle(ScreenManager.Width / 3, ScreenManager.Height / 3 + 30, ScreenManager.Width / 3, 20), "Main Menu", backgroundColor, textColor);
            menuButton.Action += GoMainMenu;

            pauseGroup.Children.Add(resumeButton);
            pauseGroup.Children.Add(menuButton);
            pauseGroup.Children[pauseGroup.SelectedIndex].Selected = true;

            pauseElements.Add(pauseGroup);

            scenes.Add("Menu", new Scene(MainMenuUpdate, MainMenuDraw));
            scenes.Add("Instructions", new Scene(InstructionsUpdate, InstructionsDraw));
            scenes.Add("Play", new Scene(PlayUpdate, PlayDraw));
            scenes.Add("Pause", new Scene(PauseUpdate, PauseDraw));
            currentScene = scenes["Menu"];
            //*********************************************************************************************///
            ///////////////////////////////////////////////////////////////////////////////////////////////////

            ////////////////////////////////////////////GAME INTIALIZATION///////////////////////////////////////
            /***************************************************************************************************/
            base.Initialize();
            Time.Initialize();
            InputManager.Initialize();
            ScreenManager.Initialize(graphics);

            //Important others depending on nothing
            random = new Random();
            textStream = new StringBuilder();
            seconds = 0.0f;
            minutes = 5;
            displayTime = minutes.ToString() + ":" + seconds.ToString();

            //HUD
            timeBox = new Label(texture, new Rectangle(ScreenManager.Width - texture.Width/2 + 50, 5, texture.Width, texture.Height),
                        displayTime, Color.White, textColor);

            speedometer = new GUIGroup();
            Rectangle speedometerOrigin;
            for (int i = 0; i < 51; i++)
            {
                if(i % 5 ==0)
                    speedometerOrigin = new Rectangle(ScreenManager.Width - 50, ScreenManager.Height - (i * 5), 50, 5);
                else
                    speedometerOrigin = new Rectangle(ScreenManager.Width - 50, ScreenManager.Height - (i * 5), 50, 1);
                GUIElement tick = new GUIElement(texture, speedometerOrigin, "", backgroundColor, textColor);
                speedometer.Children.Add(tick);
            }

            speedtick = new GUIElement(texture, new Rectangle(ScreenManager.Width - 50, ScreenManager.Height, 50, 3), "", backgroundColor, textColor);
            speedtick.Selected = true;

            textBox = new UpdatingText(null, new Rectangle(0, ScreenManager.Height/2, 100, 200), textColor, 35, 10);
            

            //Input regimes
            horizontal = new InputAxis(Keys.A, Keys.D, 1.0f, 10.0f);
            vertical = new InputAxis(Keys.W, Keys.S, 1.0f, 10.0f);
            cameraUp = new InputAxis(Keys.Up, Keys.Down, 1.0f, 1.0f);
            cameraSide = new InputAxis(Keys.Left, Keys.Right, 1.0f, 1.0f);
            cameraZoom = new InputAxis(Keys.Home, Keys.End, 1.0f, 1.0f);

            //The player
            InitPlayer();

            //The environment
            InitEnvironment();
            ///////////////////////////////////////////////////////////////////////////////////////////////////////
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            font = Content.Load<SpriteFont>("Fonts/fuhfuhfuhfont");
            texture = Content.Load<Texture2D>("Textures/BlankBar");

            sphere = Content.Load<Model>("Models/Sphere");
            foreach (ModelMesh mesh in sphere.Meshes)
                foreach (BasicEffect effect in mesh.Effects)
                    effect.EnableDefaultLighting();

            box = Content.Load<Model>("Models/Box");
            foreach (ModelMesh mesh in box.Meshes)
                foreach (BasicEffect effect in mesh.Effects)
                    effect.EnableDefaultLighting();

            heightMap = Content.Load<Texture2D>("Textures/GameAreaHeightMap");
            normalMap = Content.Load<Texture2D>("Textures/NormalMap");
            terrainEffect = Content.Load<Effect>("Effects/normalMapper");

            Texture2D is1 = Content.Load<Texture2D>("Textures/WallSlideDiagram");
            Texture2D is2 = Content.Load<Texture2D>("Textures/BallLaunching");

            instructionsSprite1 = new Sprite(is1);
            instructionsSprite2 = new Sprite(is2);

            instructionsSprite1.Position = new Vector2(0, 0);
            instructionsSprite1.Scale = 0.25f * Vector2.One;
            instructionsSprite2.Position = new Vector2(0, (instructionsSprite1.Texture.Height*instructionsSprite1.Scale.Y) -20);
            instructionsSprite2.Scale = new Vector2(0.75f, 0.65f);

            instructions = new StringBuilder();
            using (sr = new StreamReader("Content/Text/instructions.txt"))
            {
                instructions.Append(sr.ReadToEnd());
                instructions.AppendLine();
            }

            instructionsNext = new StringBuilder();
            using (sr = new StreamReader("Content/Text/instructionsNext.txt"))
            {
                instructionsNext.Append(sr.ReadToEnd());
                instructionsNext.AppendLine();
            }

            font.LineSpacing = 15;
            font.Spacing = 0.11f;
        }

        protected override void Update(GameTime gameTime)
        {
            Time.Update(gameTime);
            InputManager.Update();
            // Call the update of the "current state"
            currentScene.Update();
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            GraphicsDevice.DepthStencilState = new DepthStencilState();
            // Call the draw of the "current state"
            currentScene.Draw();
            base.Draw(gameTime);
        }

        void StartGame(GUIElement element)
        {
            currentScene = scenes["Play"];
            textBox.Timer.Start();
        }

        void ExitGame(GUIElement element)
        {
            Exit();
            return;
        }

        void MakeFullScreen(GUIElement element)
        {
            ScreenManager.Setup(!ScreenManager.IsFullScreen,
                (uint)ScreenManager.Width + 1, (uint)ScreenManager.Height + 1);
        }

        void ShowGameInstructions(GUIElement element)
        {
            currentScene = scenes["Instructions"];
        }

        void MainMenuUpdate()
        {
            foreach (GUIElement element in menuElements)
                element.Update();
        }

        void MainMenuDraw()
        {
            spriteBatch.Begin();
            foreach (GUIElement element in menuElements)
                element.Draw(spriteBatch, font);
            spriteBatch.End();
        }

        void InstructionsUpdate()
        {
            if (InputManager.KeyReleased(Keys.Escape))
                currentScene = scenes["Menu"];
        }

        void InstructionsDraw()
        {
            spriteBatch.Begin();
            instructionsSprite1.Draw(spriteBatch);
            instructionsSprite2.Draw(spriteBatch);

            spriteBatch.DrawString(font, instructions, new Vector2
                ((instructionsSprite1.Texture.Width*instructionsSprite1.Scale.X) + 5, 0), 
                textColor, 0f, Vector2.Zero, 0.75f, SpriteEffects.None, 0);

            spriteBatch.DrawString(font, instructionsNext, new Vector2
                (instructionsSprite2.Texture.Width*instructionsSprite2.Scale.X, instructionsSprite2.Position.Y), 
                textColor, 0.0f, Vector2.Zero, 0.7f, SpriteEffects.None, 0);

            spriteBatch.End();
        }

        void PlayUpdate()
        {
            if (InputManager.KeyReleased(Keys.P))
            {
                textBox.Timer.Stop();
                currentScene = scenes["Pause"];
            }

            //Update GUIElements
            seconds -= Time.ElapsedGameTime;
            if (seconds < 0)
            {
                minutes--;
                seconds = 60.0f;
            }
            displayTime = minutes.ToString() + ":" + seconds;
            timeBox.Text = displayTime;

            CharacterController cc = (CharacterController)player.GetComponent(typeof(CharacterController));
            speedtick.Bounds = new Rectangle(ScreenManager.Width - 50, ScreenManager.Height - (int)cc.MoveSpeed * 5 - 2, speedtick.Bounds.Width, speedtick.Bounds.Height);

            textBox.Update();


            //Handle inputs
            horizontal.Update(); vertical.Update();
            cameraUp.Update(); cameraSide.Update(); cameraZoom.Update();

            //Apply Gravity-preferably:
            //if(player.isJumpin)
            //  verticalSpeed -= gravity;

            //Move the player
            Rigidbody r = (Rigidbody)player.GetComponent(typeof(Rigidbody));
            /*r.Velocity += (movementObject.Transform.Forward * vertical.InputValue * moveSpeed) + (camera.Transform.Left * horizontal.InputValue * moveSpeed);
            r.Velocity += new Vector3(0, verticalSpeed, 0);
            //Dun wanna move foeva
            r.Velocity *= 0.95f;*/

            //Handle Collisions
            SphereCollider s = (SphereCollider)player.GetComponent(typeof(SphereCollider));
            /*BoxCollider b = (BoxCollider)building.GetComponent(typeof(BoxCollider));
            if (b.Collides(s, out normal))
            {
                Vector3 newDirection = b.Host.Transform.Up;
                newDirection = Vector3.Cross(newDirection, normal);
                float dot = Vector3.Dot(r.Velocity, newDirection);
                r.Velocity = newDirection * dot;
            }*/

            foreach (GameObject obj in buildings)
            {
                BoxCollider b = (BoxCollider)obj.GetComponent(typeof(BoxCollider));
                if (b.Collides(s, out normal))
                {
                    Vector3 newDirection = b.Host.Transform.Up;                 //We are having issues with collisions in the corners
                    newDirection = Vector3.Cross(newDirection, normal);         //of the box colliders. See the line:
                    float dot = Vector3.Dot(r.Velocity, newDirection);          //float d = Math.Abs(Vector3.Dot(sc.Transform.Position - x, normal))
                    r.Velocity = newDirection * dot;                            //in BoxCollider to find the reason why
                }
            }

            TerrainCollider tc = (TerrainCollider)terrain.GetComponent(typeof(TerrainCollider));
            if (tc.Collides(s, out normal))
            {
                //Use the out normal to extrude the sphere from the terrain geometry following collision
                s.Host.Transform.LocalPosition += normal;
                cc.grounded = true;
                cc._characterState = CharacterController.CharacterState.Idle;
                cc.VerticalSpeed = 0;
                Console.WriteLine("yep");
            }

            //Camera Controls, and a clamped vertical rotation axis
            float cameraInput = cameraUp.InputValue * cameraSpeed;
            rotation += cameraInput;
            if (rotation > Math.PI / 4 || rotation < 0)
            {
                rotation -= cameraInput;
            }
            else
                cameraObject.Transform.Rotate(camera.Transform.Left, cameraInput);

            cameraInput = cameraSide.InputValue * cameraSpeed;
            cameraObject.Transform.Rotate(player.Transform.Up, cameraInput);

            cameraInput = cameraZoom.InputValue * cameraSpeed;
            zoom += cameraInput;
            if (zoom > (2 * Math.PI / 3) || zoom < 0)
            {
                zoom -= cameraInput;
            }
            else
                camera.FieldOfView = zoom;

            cc.Update();
        }

        void PlayDraw()
        {
            //Render the 3D scene
            Renderer r = (Renderer)player.GetComponent(typeof(Renderer));
            r.Draw(camera, ScreenManager.GraphicsDevice);
            CharacterController cc = (CharacterController)player.GetComponent(typeof(CharacterController));

            foreach (GameObject obj in buildings)
            {
                r = (Renderer)obj.GetComponent(typeof(Renderer));
                r.Draw(camera, ScreenManager.GraphicsDevice);
            }

            r = (TerrainRenderer)terrain.GetComponent(typeof(TerrainRenderer));
            r.Material.Update(terrain.Transform.World, camera, light, Color.LightGreen,
                8.0f, 0, 1);
            r.Draw();

            //Draw 2D elements
            spriteBatch.Begin();
            textBox.Draw(spriteBatch, font);
            speedometer.Draw(spriteBatch, font);
            speedtick.Draw(spriteBatch, font);
            timeBox.Draw(spriteBatch, font);
            spriteBatch.DrawString(font, cc.MovementAxis.LocalRotation.ToString(), new Vector2(0, 25), Color.Black);
            spriteBatch.DrawString(font, cc.MoveDirection.ToString(), Vector2.Zero, Color.Black);
            spriteBatch.DrawString(font, cc.CameraAxis.Forward.ToString(), new Vector2(ScreenManager.Height/2, 50), Color.Black);
            spriteBatch.DrawString(font, cameraObject.Transform.Forward.ToString(), new Vector2(ScreenManager.Height/2, 75), Color.Black);
            spriteBatch.End();
        }

        void GoMainMenu(GUIElement element)
        {
            currentScene = scenes["Menu"];
        }

        void PauseUpdate()
        {
            foreach (GUIElement element in pauseElements)
                element.Update();
        }

        void PauseDraw()
        {
            Renderer r = (Renderer)player.GetComponent(typeof(Renderer));
            r.Draw(camera, ScreenManager.GraphicsDevice);

            foreach (GameObject obj in buildings)
            {
                r = (Renderer)obj.GetComponent(typeof(Renderer));
                r.Draw(camera, ScreenManager.GraphicsDevice);
            }

            r = (TerrainRenderer)terrain.GetComponent(typeof(TerrainRenderer));
            r.Material.Update(terrain.Transform.World, camera, light, Color.LightGreen,
                8.0f, 0, 1);
            r.Draw();

            spriteBatch.Begin();
            foreach (GUIElement element in pauseElements)
                element.Draw(spriteBatch, font);
            spriteBatch.End();
        }

        private void InitPlayer()
        {
            //Player details
            player = new GameObject();
            player.Transform = new Transform();
            //player.Transform.LocalPosition += Vector3.Backward * 100;
            player.Transform.LocalPosition += Vector3.Up * 10;

            movementObject = new GameObject();
            movementObject.Transform = new Transform();
            movementObject.Transform.Parent = player.Transform;

            player.Add(new Renderer(sphere));

            player.Add(new Rigidbody());
            Rigidbody r = (Rigidbody)player.GetComponent(typeof(Rigidbody));
            r.Transform = player.Transform;

            player.Add(new SphereCollider());
            SphereCollider s = (SphereCollider)player.GetComponent(typeof(SphereCollider));
            s.Transform = player.Transform;

            //Initialize Character Controller
            player.Add(new CharacterController());
            CharacterController cc = (CharacterController)player.GetComponent(typeof(CharacterController));
            cc.Transform = player.Transform;

            //I've deliberately chosen to do all of this outside of a constructor for clarity and to run the different
            //properties through my brain. Likely I will implement one later on when I am familiar with all components
            cc.RunSpeed = 6.0f;
            cc.JumpHeight = 1.0f;
            cc.Gravity = 9.8f;

            cc.Horizontal = horizontal;
            cc.Vertical = vertical;

            //Those cameras
            camera = new Camera();
            camera.Transform = new Transform();
            cameraObject = new GameObject();
            cameraObject.Transform = new Transform();
            cameraObject.Transform.Parent = player.Transform;

            //Starting out with a third person camera
            camera.Transform.Parent = cameraObject.Transform;
            camera.Transform.LocalPosition = new Vector3(0, 0, 30);
            player.Add(camera);

            cc.CameraAxis.Parent = cameraObject.Transform;
       }

        private void InitEnvironment()
        {
            //And illumination
            light = new Light(Color.Gray, 1.0f);
            light.Transform = camera.Transform;

            //Making environment
            terrain = new GameObject();
            terrain.Transform = new Transform();
            terrain.Transform.LocalScale += Vector3.Up * 10.0f;
            terrain.Add(new TerrainRenderer(heightMap, new Vector2(1000, 1000), Vector2.One * 1000));

            TerrainRenderer renderer = (TerrainRenderer)terrain.GetComponent(typeof(TerrainRenderer));
            renderer.Transform = terrain.Transform;
            terrainMaterial = new Material(terrain.Transform.World, camera, light, Color.Green,
                50.0f, 0, 1, terrainEffect, normalMap);
            renderer.Material = terrainMaterial;

            terrain.Add(new TerrainCollider(renderer));

            float zeroHeight = Single.MaxValue;
            for (int i = 0; i < renderer.Heights.Length; i++)
                if (renderer.Heights[i] * terrain.Transform.LocalScale.Y < zeroHeight)
                    zeroHeight = renderer.Heights[i] * terrain.Transform.LocalScale.Y;

            terrain.Transform.LocalPosition = new Vector3(0, -zeroHeight, 0);

            buildings = new List<GameObject>();
            for (int i = 0; i < 20; i++)
                AddBuilding();

            /*
            building = new GameObject();
            building.Transform = new Transform();
            building.Transform.LocalPosition = Vector3.Left * 10;
            building.Add(new BoxCollider());
            BoxCollider b = (BoxCollider)building.GetComponent(typeof(BoxCollider));
            b.Transform = new Transform();
            b.Transform.Parent = building.Transform;
            b.Size = building.Transform.LocalScale;
            building.Add(new Renderer(box));
            
            r = (Renderer)building.GetComponent(typeof(Renderer));
            r.Draw(camera, ScreenManager.GraphicsDevice);
            */
        }

        private void AddBuilding()
        {
            building = new GameObject();
            building.Transform = new Transform();
            building.Transform.LocalPosition = new Vector3(random.Next(-100, 100),
                                               0, random.Next(-100, 100)); ;

            building.Add(new BoxCollider());
            BoxCollider b = (BoxCollider)building.GetComponent(typeof(BoxCollider));
            b.Transform = building.Transform;
            b.Size = building.Transform.LocalScale;

            building.Add(new Renderer(box));

            buildings.Add(building);
        }
    }
}